#######################################
####
####	fdstools.pm  - Perl module for manipulating various elements of a Fedora Directory Server
####
#######################################

#	02/24/2009 - added some more perldoc help until I got sick of it :P
#	02/23/2009 - added a couple get_ subs to get access to the $config options from the calling script
#	02/19/2009 - finished moving TLS/SSL subs into module.
#	02/11/2009 - started moving fdsssl.pl subs into this module.
#	02/10/2009 - major rewrite,  moving most subs to this fdstools.pm module
#			- ended up rewriting the majority of subs to work in a oo type perl module.
#	NOTES below here are pretty much depricated,  but leaving them for now.
#	09/26/2008 - added URI string conversion for add_replica_referral  ** NOT IN NEW MODULE YET **
#	09/22/2008 - removed the creation of the referral entry when creating SMR setup. 
#	09/10/2008 - made some fixes so the script would work with fds 1.1+
#	09/10/2008 - changed app name to repmanager.pl and created fdstools package

#  This list is from before I moved the script to perl module.  So lots of processing logic is not in the module anymore,
#  TODO list
#	1. implement replication agreement removal
#	2. some sort of complete server removal routine that will blow away reps/referrals
#		across all servers if you are perm removing a server 
#	3. add/remove servers right from the menu rather then editing the config file
#	4. proper perl doc config
#	5. add Single Master option for creating replication objects
#	6. fix get_referrals sub not to puke when no referrals exist <--- IMPORTANT
#	7. add a bunch of ldap routines to query cn=SNMP, cn=config and output in a nice format for graph generation

# requires the following perl packages in debian.
#		libnet-ldap-perl  
#		libterm-readkey-perl
#		


# based on concepts/code from

# mmr.pl - configure multimaster replication between two fedora-ds servers
#
# Mike Jackson <mj@sci.fi> 19.11.2005
# Federico Roman added the --port option. 10-08-2007
#
# Professional LDAP consulting for large and small projects
#
#       http://www.netauth.com
#
# GPLv2 License
#


package fdstools;

use strict;
use File::Copy;
use Net::LDAP qw(LDAP_ALREADY_EXISTS LDAP_TYPE_OR_VALUE_EXISTS);
use Term::ReadKey;
use Sys::Hostname::Long;
use Data::Dumper;

our $VERSION = '0.01';


sub new {
	my $class = shift;

        # create a hash of our remaining parameters
	my %args = @_;

	# create the config anonymous hashref
	my $config = {};
	# bless it early?
	bless ($config, $class);
	
	# initialize some values in the config hash
	# set the system defaults config file
	$config->{FDST_DEFAULT_CONFIG} = "/etc/fdstools/fdstools.conf";
	# set the default host specific config file   
	$config->{FDST_CONFIG} = "/etc/fdstools/repman.conf";
	# initialize some lists
	$config->{key_serial} = [];
	$config->{key_hostname} = [];
	$config->{key_description} = [];
	
	
	# assign an alternate config file if needed
	if (defined( $args{config} ) ) {
		$config->{FDST_CONFIG} = $args{config};
	}
	
	# add any of the remaining arguements to the config hash
	while ( my ($key, $value) = each(%args) ) {
		#print "$key => $value\n";
		$config->{$key} = $value;
	}

	# load up the 2 config files.
	load_file($config, $config->{FDST_DEFAULT_CONFIG});
	load_file($config, $config->{FDST_CONFIG});
	
	return $config;
}
=head1 NAME

fdstools - Perl module to help with the creating and configuring of Replication and Encryption on Fedora Directory Servers.
	

=head1 VERSION

Version 0.01

=cut

=head1 SYNOPSIS

Quick summary of what the module does.

Perhaps a little code snippet.

    use fdstools;

    my $foo = fdstools->new();
    $foo->add_rep_user("fdsserver1.example.com");
    $foo->add_rep_user("fdsserver2.example.com");
    $foo->add_changelog("fdsserver1.example.com");
    $foo->add_changelog("fdsserver2.example.com");
    $foo->add_replica_object("fdsserver1.example.com" ,"1");
    $foo->add_replica_object("fdsserver2.example.com" ,"1");
    $foo->add_rep_agreement("fdsserver1.example.com" ,"1" ,"fdsserver2.example.com", "636");
    $foo->add_rep_agreement("fdsserver2.example.com" ,"1" ,"fdsserver1.example.com", "636");	
    $foo->init_replication("fdsserver1.example.com","fdsserver2.example.com");
    ...

=head1 FUNCTIONS

#############################
###
### replication subs
###
#############################

=head2 add_changelog

This function will create the changelog object.

This function takes only one paramater.  A server to create the changelog object on.  The server string will accept a : delimmited port,  will assume 389 if none used.

add_changelog("fdsserver1.example.com");
or
add_changelog("fdsserver1.example.com:389");

=cut

sub add_changelog
{
	# adds the changelog entry if required
	my $config = shift;
	my $server = shift;
	my ($ldap, $msg, $dn);

	
	if ( ($ldap = conn_bind($config, $server)) eq 0 ) {
		print "\t*********** bind/connect failed to $server ***************\n";
		return 0;
	}
	##############################
	# find the instance-dir
	##############################
	$msg = $ldap->search (
                          base    => "cn=config, cn=ldbm database, cn=plugins, cn=config",
                          scope   => "base",
                          filter  => "(objectClass=*)",
                         );

	my $instance_dir = $msg->entry(0)->get_value("nsslapd-directory");
	# check to make sure we got something back
	if (!defined($instance_dir)) {
		print "Unable to determine the instancedir,  pretty big issue,  puking\n";
		exit 1;
	}	
	# get the actual instancedir by removing /db
	$instance_dir =~ s/\/db$//;

	##############################
	# add changelog
	##############################
	$dn = "cn=changelog5,cn=config";
	print "adding changelog object to $server -> $dn\n";
	$msg = $ldap->add(
		$dn,
		attr => [
		objectclass                  => [qw (top extensibleObject)],
		cn                           => "changelog5",
		"nsslapd-changelogdir"       => "$instance_dir/changelogdb",
		]
	);

	if ($msg->code == LDAP_ALREADY_EXISTS)
	{
		print "\t -> already exists\n\n";
	} else {
		$msg->code && die "failed to add changelog entry: " . $msg->error;
	}


	disconnect($ldap);

}


=head2 add_replica_object

This function will create the replication object,  which sets which type of replication we're doing.  single-single,  MMR etc.
This function requires at least 2 paramaters,  the supplier server and replication type,  depending on replication type
you may need to include more parameters.

add_replica_object(SERVER, REPLICA_TYPE, REFERRAL_SERVER, REFERRAL_PORT)

rep_type parameter is types

"1" for a MMR replica object
"2" for a SMR replica object	

to create a replica object for use with a MMR server
add_replica_object("fdsserver1",1")

If you want to create a replica object for a consumer server you need to include the referral server info
add_replica_object("fdsserver1", "1", "fdsserver2","389");

Would create a read only replica on fdsserver1 with writes being referred to fdsserver2 on port 389.


=cut

sub add_replica_object
{
	
	# nsDS5ReplicaType
	# 0 - reserved for internal use
	# 1 - Dedicated Supplier
	# 2 - Dedicated Consumer	<---- consumer only
	# 3 - Supplier/Consumer		<---- MMR server
	
	# passed variable $rep_type is not the same as the above!!
	# $rep_type = 1 == MMR
	# $rep_type = 2 == SMR

	# TODO should fix all that and add the option for SMR
	# TODO do a search for BASE_DN and see if it exists,  if not create it first.
	# otherwise fds replication will freak out (with good reason :P )
	my $config = shift;
	my ($server,  $rep_type, $referral, $referral_port) = @_;
	my ($ldap, $msg, $dn);
	
	# server is the server the object is getting created on
	# referral is the server to send referrals to if setting up a consumer read only
	if ( ($ldap = conn_bind($config, $server)) eq 0 ) {
		print "\t*********** bind/connect failed to $server  ***************\n";
		return 0;
	}
	##############################
	# add replica object
	##############################

	# type 1 is MMR
	# type 2 is consumer read only
	$dn = "cn=replica,cn=\"$config->{BASE_DN}\",cn=mapping tree,cn=config";
    
	print "adding to Replica object to $server -> $dn\n";
	
	# add consumer type rep
	if ( $rep_type eq 2 ) 	{
		$msg = $ldap->add(
			$dn,
			attr => [
				objectclass                  => [qw (top nsDS5Replica extensibleObject)],
				cn                           => "replica",
				nsDS5ReplicaId               => 65535,
				nsDS5ReplicaReferral	     => "ldap://$referral:$referral_port/$config->{BASE_DN}",
				nsDS5ReplicaRoot             => $config->{BASE_DN},
				nsDS5Flags                   => 0,
				nsDS5ReplicaBindDN           => $config->{REP_DN},
				nsds5ReplicaPurgeDelay       => 604800,
				nsds5ReplicaLegacyConsumer   => "off",
				nsDS5ReplicaType             => 2,
			]
		);
	}

	# add MMR type rep
	if ($rep_type eq 1) {
		print "Trying to create $dn\n\n";
		$msg = $ldap->add(
			$dn,
			attr => [
				objectclass                  => [qw (top nsDS5Replica extensibleObject)],
				cn                           => "replica",
				nsDS5ReplicaId               => $config->{REP_ID},
				nsDS5ReplicaRoot             => $config->{BASE_DN},
				nsDS5Flags                   => 1,
				nsDS5ReplicaBindDN           => $config->{REP_DN},
				nsds5ReplicaPurgeDelay       => 604800,
				nsds5ReplicaLegacyConsumer   => "off",
				nsDS5ReplicaType             => 3,
			]
		);

		
	}

	if ($msg->code == LDAP_ALREADY_EXISTS)
	{
		print "\t -> already exists\n\n";
		# disconnect and return here before incrementing the rep_id if the object already exists
		disconnect($ldap);
		return;
		
	} else {
		$msg->code && die "failed to add replica entry: " . $msg->error;
	}

	# if MMR reptype,  and we created an object,  we need to increment the rep_id in the config file
	if ( $rep_type eq 1 ) {
		$config->{REP_ID} += 1;
		save_file($config, $config->{FDST_DEFAULT_CONFIG});
		}
	
	disconnect($ldap);

}


=head2 add_rep_agreement

Function to create the replication agreement on the supplier.  There are 4 required parameters

add_rep_agreement(SUPPLIER, WITH_SSL, CONSUMER, CONSUMER_PORT)

SUPPLIER -  the supplier server in the agreement

WITH_SSL -  Whether or not to use SSL for the replication agreement.  1 for yes 0 for no.

CONSUMER -  the consumer server in the agreement

CONSUMER_PORT -  the target port on the consumer server.

add_rep_agreement("fdsserver1.example.com","1",fdsserver2.example.com","636")

Would create a SSL enabled replication agreement from fdsserver 1 > fdsserver2.

If you are adding a Mult-Master replication agreement,  you would need to run the function twice

add_rep_agreement("fdsserver1.example.com","1",fdsserver2.example.com","636")

add_rep_agreement("fdsserver2.example.com","1",fdsserver1.example.com","636")

To create the rep agreement on the second server aswell.

=cut

sub add_rep_agreement
{
	my $config = shift;
	my ($supplier, $with_ssl, $consumer, $consumer_port ) = @_;
	my ($ldap, $msg, $dn, $cn);

	#$ldap = conn_bind($source, $source_port, $bind_pw);
	if ( ($ldap = conn_bind($config, $supplier)) eq 0 ) {
		print "\t*********** bind/connect failed to $supplier ***************\n";
		return 0;
	}
	print "adding Replication Agreement $supplier ==> $consumer\n";

	$dn = "cn=Replication of $config->{DN_DESC} to $consumer,cn=replica,cn=\"$config->{BASE_DN}\",cn=mapping tree,cn=config";
	$cn = "Replication of $config->{DN_DESC} to $consumer";
	# I used to add the non ssl agreement then modify it to enable ssl.  That seems to bork the server
	# I think as soon as the agreement is created,  it tries to connect to the consumer on the specified port
	# well for a ssl agreement it will try to connect to port 636 with a non encrypted session and throw errors.
	#  restarting dirsrv after adding the SSL option would fix it,  but having to restart after adding replication is weak.
	if ($with_ssl eq 1) {
		# add SSL enabled rep
		$msg = $ldap->add(
			$dn,
				attr => [
				objectclass                  => [qw (top nsDS5ReplicationAgreement)],
				cn                           => $cn,
				Description		     => $cn,
				nsDS5ReplicaHost             => $consumer,
				nsDS5ReplicaRoot             => $config->{BASE_DN},
				nsDS5ReplicaPort             => $consumer_port,
				nsDS5ReplicaTransportInfo    => "SSL",
				nsDS5ReplicaBindDN           => $config->{REP_DN},
				nsDS5ReplicaBindMethod       => "simple",
				nsDS5ReplicaCredentials      => $config->{REPMAN_PW},
				# dont include any update schedule to always have it update
				#nsDS5ReplicaUpdateSchedule   => "0000-2359 0123456",
				nsDS5ReplicaTimeOut          => 120,
			]
		);
	} else {
		# without SSL
		$msg = $ldap->add(
			$dn,
				attr => [
				objectclass                  => [qw (top nsDS5ReplicationAgreement)],
				cn                           => $cn,
				Description		     => $cn,
				nsDS5ReplicaHost             => $consumer,
				nsDS5ReplicaRoot             => $config->{BASE_DN},
				nsDS5ReplicaPort             => $consumer_port,
				#nsDS5ReplicaTransportInfo    => "SSL",
				nsDS5ReplicaBindDN           => $config->{REP_DN},
				nsDS5ReplicaBindMethod       => "simple",
				nsDS5ReplicaCredentials      => $config->{REPMAN_PW},
				# dont include any update schedule to always have it update
				#nsDS5ReplicaUpdateSchedule   => "0000-2359 0123456",
				nsDS5ReplicaTimeOut          => 120,
			]
		);
	}

	if ($msg->code == LDAP_ALREADY_EXISTS) {
		print "\t -> already exists\n\n";
		# disconnect here and return if exists
		disconnect($ldap);
		return;

	} else {
		$msg->code && die "failed to add replication agreement entry: " . $msg->error;
        }

# 	if ($with_ssl eq 1) {
# 		# enable ssl on this agreement.
# 		$msg  = $ldap->modify(
# 			$dn,
# 				add	=> { 'nsDS5ReplicaTransportInfo' => "SSL" } );
# 	}
	
	disconnect($ldap);
	
}


=head2 add_rep_user

Function to add the Replication Manager account for the replication proccess to auth against.  Only takes one parameter.

add_rep_user(SERVER);

SERVER - the directory server to add the user on.  defaults to port 389 or specify by adding :PORTNAME

add_rep_user("fdsserver1.example.com")   OR  add_rep_user("fdsserver1.example.com:636");


=cut
sub add_rep_user
{
	
	my $config = shift;
	my $server = shift;
	
	my ($ldap, $msg);
	
	if ( ($ldap = conn_bind($config, $server)) eq 0 ) {
		print "\t*********** bind/connect failed to $server ***************\n";
		return 0;
	}

	print "adding $config->{REP_DN} to $server\n";
	
	$msg = $ldap->add($config->{REP_DN},
          		attr => [
			# needed to add organizationalPerson inetorgperson or ldap would throw err=65 when trying to add
				objectclass	=> [qw (top person organizationalPerson inetorgperson)],
				cn		=> "RManager",
				sn		=> "RManager",
				userPassword	=> $config->{REPMAN_PW},
          		]
		);
	if ($msg->code == LDAP_ALREADY_EXISTS)
	{
		print "\t -> already exists\n\n";
	} else {
        	$msg->code && die "failed to add $config->{REP_DN} entry: " . $msg->error;
	}
	
	disconnect($ldap);

}

=head2 init_replication

Function to initialize a replication agreement between 2 servers.

init_replication(SUPPLIER, CONSUMER)

Even when using MMR rep,  set the second MMR server as the consumer

=cut

# TODO filter the $supplier and $consumer strings to see if a :PORT value exists,
#      if it does,  it will breake the $dn value.
sub init_replication
{
	# this will set the initialize attribute in the rep agreement on the supplier
	# the supplier should then begin initializing the target
	my $config = shift;
	my ($supplier,$consumer) = @_;
	my ($ldap, $msg);

	if ( ($ldap = conn_bind($config, $supplier)) eq 0 ) {
		print "\t*********** bind/connect failed to $supplier ***************\n";
		return 0;
	}
	# TODO make sure the new DN with DN_DESC is working
	my $dn = "cn=Replication of $config->{DN_DESC} to $consumer,cn=replica,cn=\"$config->{BASE_DN}\",cn=mapping tree,cn=config";

	print "\nInitializing replication $supplier -> $consumer\n";
	
	$msg = $ldap->modify($dn, add => { 'nsDS5BeginReplicaRefresh' => 'start' });
	$msg->code && die "failed to add initialization attribute: " . $msg->error;

	disconnect($ldap);
}

=head2 save_file

Internal function to save a given key="value" style config file

takes two  parameters,  the $config hash and the file to save to.

save_file($config, FILENAME)

=cut

sub save_file
{
	my $config = shift;
	my $conf_file = shift;
	# TODO read/write checks on %tmpfile
	my $tmpfile = "/tmp/repman.conf.tmp";
	open CONFIG, $conf_file;
	open NEWCONFIG, ">$tmpfile";

	while (<CONFIG>) {
		# If an empty line is detected,  echo one to the new config file.
		if ( m/^\s+/ ) {
			print NEWCONFIG "\n";
			next;
		}
		# print any commented lines to the new file.
		if ( m/^#/ ) {
			print NEWCONFIG $_;
			next;
		} 
		# everything else should be valid options
		# grab a key from the old config file,  then take that value from $config hash and output it to the tempfile
		# should stop from dumping the complete contents of $config to the config file.
		my ($key,$value) = split(/=/);
		print NEWCONFIG "$key=\"" . $config->{ $key } . "\"\n";
		
	}
	

	close CONFIG;
	close NEWCONFIG;

	# back up old file,  then copy the new one over.
	copy ( $conf_file, "$conf_file.old");
	copy ($tmpfile, $conf_file);
	unlink( $tmpfile );


}
=head2 load_file

Internal function to load a given key="value" style config file and populate the $config hash.

takes two paramaters,  the $config hashref and the file to load

load_file($config, FILENAME)

=cut
sub load_file
{
	my $config = shift;
	my $conf_file = shift;
	# check to make sure file is readable/writable.
	check_file($conf_file);
	
	open CONFIG, $conf_file;
		
	while (<CONFIG>) {
		chomp;                  # no newline
        	s/#.*//;                # no comments
		s/^\s+//;               # no leading white
		s/\s+$//;               # no trailing white
		next unless length;     # anything left?
		my ($var, $value) = split(/\s*=\s*/, $_, 2);
		# strip quotes off of value
		$value =~ s/"//;
		$value =~ s/"$//;
		$config->{$var} = $value;
		
	} 
	close CONFIG;

}
=head2 check_file

Internal function make sure a passed filename is both readable AND writeable.

takes two paramaters,  the $config hashref and the file to check

check_file($config, FILENAME)

=cut

sub check_file
{
	# check the read/write status of a file passed
	my $file = shift;
	print "checking $file\n";
	if ( ! -r $file ) {
	      print "ERROR:  $file doesn't exist or is not readable for this user! Quitting\n";
	 
	      exit 100;
 	}
	if ( ! -w $file ) {
	      print "ERROR: $file is not writeable for this user! Quitting\n";
	      exit 101;
	}

}

##########
##  conn_bind - This function will both connect and bind to the server parameter it is passed.
##		Will bind using the config file parameter  BIND_DN 
##		TLS INFO
##		Connecting via TLS can happen in one of 2 ways. TLS=1 is enabled in the system defaults config file
##		or when creating the fdstools object you passed a use_tls => "1" parameter.
##		---
##		This function will always try to find out if it was passed a port in the server string.
##########
=head2 conn_bind

Internal function to create ldap objects to search/modify against.

Takes two paramaters,  the $config hashref and the server string to connect to.  Can be : delimmited with the port on the end

conn_bind will check the config hash to see if either $config->TLS=1 (via fdstools.conf) or $config->use_tls=1 (parameter passed when creating module.)

This function will return a NET::ldap object.

my $ldap = conn_bind($config,SERVER[:PORT])

=cut

sub conn_bind
{
	# sub to connect and bind to the passed server
	my $config = shift;
	my $server = shift;
	
	# If we haven't initialized the $config->{BIND_PW} parameter,  run find_bind_pw
	if (! defined($config->{BIND_PW})) {
		find_bind_pw($config);
	}

	my ($msg,$ldap); 
	print "in conn_bind ==>$server<==\n\n";
	# if the server string contains a colon,  split it and use the requested port,  otherwise default to 389
	if ( $server =~ m/:/ ) {
			my ($url, $port) = split(/:/,$server);
			$ldap = Net::LDAP->new($url, port => $port, timeout=>10);
	} else {
		# default to 389
		$ldap = Net::LDAP->new($server, port => '389', timeout=>10);
	}

	if ( ! $ldap )
	{
			
			# return 0 here for a failed attempt
			return 0;
	}
	

	# encrypt if specified on commandline or in configs
	# TODO add a OR option here to look for $config->{go_tls}, the argument way of starting TLS
	if ( $config->{TLS} ) {
		$msg = $ldap->start_tls(verify => 'none',capath => $config->{CA_DIR});	 
 	 		if ( $msg->is_error() ) {
 	 			print "Bailed on server $server return code was " . $msg->code() . "\n";
 	 			print "error message is " . $msg->error(). "\n";
 	 			print "error_name - " . $msg->error_name . "\n\n" . "error_text is " . $msg->error_text . "\n";
				print "\n\n\nTLS HAS FAILED, QUITTING AND EITHER FIX OR TURN OFF TLS!!!!\n\n\n";
				exit;
 	 		}
 	}

	# attempt to bind
	#print "ABOUT TO BIND WITH $config->{BIND_DN} and $config->{BIND_PW}!!!\n";
	$msg = $ldap->bind ( $config->{BIND_DN},password => $config->{BIND_PW} ,version => 3 );
	#$msg->code && die $msg->error;

	if ($msg->code) {
		# print error message here because we have access to the $msg object
		print "\t$server\t\t\tFAILURE " . $msg->code . " error text is " . $msg->error_name . "\n";
		
		return 0;
	}

	return $ldap;
	
}

=head2 disconnect

Internal function to tear down $ldap objects nicely

takes one parameter,  a NET::ldap object.

check_file($config, FILENAME)

=cut

sub disconnect
{
	
	my ($ldap) = shift;
	$ldap->unbind;
	$ldap->disconnect;
}

##########
##  find_bind_pw - function to determine what the BIND_DN user's password is.
##		   by default,  it will look in the files defined by 
#		   PAM_SECRET NSS_SECRET in fdstools.conf.
##########

=head2 find_bind_pw

Internal function to determine where to get the BIND_PW from

Will first check to see if $config->{prompt_pw} is set to 1,  if not it will then look in $config->{PAM_SECRET} then $config->{NSS_SECRET}.

If none of those are able to get the password,  it will default to prompting you.

Takes only one parameter,  the $config hashref

=cut

sub find_bind_pw
{
	my $config = shift;
	# sub to figure out where the bind dn password is located.
	# this whole program assumes the same bind dn password accross all servers!!
	# the file checks are configurable in the .conf file
	
	# prompt the use to enter the bind password if requested
	# have it first so we can override the files by asking it to prompt.
	if (defined($config->{prompt_pw} ) ) {
		print "Root DN password:";
		# use Term::ReadKey; to disable the password echo to the screen.
		ReadMode('noecho'); # don't echo
		chomp($config->{BIND_PW} =(<STDIN>));
		ReadMode(0);        # back to normal
		return 0;
	}

	# else check for a password in the next 2 files.
	# check if the libpam.secret file exists and is not zero sized
	if ( ( -e $config->{PAM_SECRET}) && (! -z $config->{PAM_SECRET}) ) {
		open PW, $config->{PAM_SECRET};
		chomp($config->{BIND_PW} =(<PW>));
		close PW;
		return 0;
		
	}
	# check if the libnss-ldap.secret  file exists and is not zero sized
	if ( ( -e $config->{NSS_SECRET}) && (! -z $config->{NSS_SECRET}) ) {
		open PW, $config->{NSS_SECRET};
		chomp($config->{BIND_PW} =(<PW>));
		close PW;
		return 0;
		
	}
	
	# if we've made it here,  we didn't find any password,  and didn't specify to prompt
	# we could prompt by default,  or quit with error.
	
	print "Root DN password:";
	# use Term::ReadKey; to disable the password echo to the screen.
	ReadMode('noecho'); # don't echo
	chomp($config->{BIND_PW} =(<STDIN>));
	ReadMode(0);        # back to normal
	return 0;

}
=head2 dump_info

Function to dump all relevent replication info for a given server to the terminal

#################################################
######### Info dump subs
#################################################
=cut

sub dump_config {
	
	my $config = shift;
      # dump $config contents to the screen
      while( my ($k, $v) = each %$config ) {
        print "key: $k, value: $v.\n";
	}
      print "\n\n";

}
sub dump_info
{
	my $config = shift;
	my $server = shift;
	my ($ldap, $msg);
	
	
	if ( ($ldap = conn_bind($config, $server)) eq 0 ) {
		print "\t*********** bind/connect failed to $server *******ASS********\n";
		return 0;
	}

	$msg = $ldap->search(filter=>"objectClass=nsDS5Replica", 
			 base=>"cn=config");
	print "Dumping Replica agreement on $server\n\n";
	foreach my $entry ($msg->entries) { $entry->dump; print "\n\n"; }
	
	$msg = $ldap->search(filter=>"objectClass=nsDS5ReplicationAgreement", 
			 base=>"cn=config");
	print "\nDumping all Replication agreements on $server\n\n";
	foreach my $entry ($msg->entries) { $entry->dump; print "\n\n"; }

	
	$msg = $ldap->search(filter=>"cn=changelog5", 
			 base=>"cn=config");
	print "\nDumping changelog object on $server\n\n";
	foreach my $entry ($msg->entries) { $entry->dump; print "\n\n"; }

	$msg = $ldap->search(filter=>"objectClass=nsMappingTree", 
			 base=>"cn=config");
	print "\nDumping mapping tree object on $server\n\n";
	foreach my $entry ($msg->entries) { $entry->dump; print "\n\n"; }

	$msg = $ldap->search(filter=>"uid=RManager", 
			 base=>"cn=config");
	print "\nDumping mapping tree object on $server\n\n";
	foreach my $entry ($msg->entries) { $entry->dump; print "\n\n"; }


}

###########################################
#########
#########  TLS/SSL config subs
#########
###########################################

##########
##  create_pwdfile - create (if needed) the pwdfile
##########

=head2 create_pwdfile

Function to check on the status of the pwdfile used by the certificate db

You can enter your own value,  or it will try to use some rather Linux specific system calls to generate random cruft.

Takes only one parameter,  the $config hashref

=cut
sub create_pwdfile {

	my $config = shift;

	print "\t\tChecking status of pwdfile\n";
	if ( ! -f "$config->{SEC_DIR}/pwdfile" ) {
		print "No pwdfile detected, enter password or hit enter for random [RANDOM]: ";
		chomp(my $pwd = <STDIN>);
		if ( $pwd eq "" ) {
			my $ret = system("(ps -ef ; w ) | sha1sum | awk \'{print \$1}\' > $config->{SEC_DIR}/pwdfile");
			if ($ret > 0) {
			print "Error occured during pwdfile generation,  quitting\n";
			exit 1;
			}
		} else {
			# change this to a perl open then print statments eventually
			open PWD, ">$config->{SEC_DIR}/pwdfile";
			#my $ret = system("echo $pwd > $config{SEC_DIR}/pwdfile");
			print PWD $pwd;
			close PWD;
		}

	} else {
		print "\tUsing existing pwdfile\t\t--->$config->{SEC_DIR}/pwdfile<---\n";
	}
	# must make sure the pwd file is owned and readable by the user fds runs as ONLY
	# determine user as the one who owns $config{SEC_DIR}
	my @stat = stat($config->{SEC_DIR});
	chown $stat[4], $stat[5], "$config->{SEC_DIR}/pwdfile";
	chown $stat[4], $stat[5], "$config->{SEC_DIR}/pin.txt";
	chmod 0400, "$config->{SEC_DIR}/pwdfile";
	chmod 0600, "$config->{SEC_DIR}/pin.txt";

}

##########
##  create_noisefile - create (if needed) a noise file.
##########

=head2 create_noisefile

Function to check on the status of the noisefile used by the certificate db

It will try to use some rather Linux specific system calls to generate random cruft.

Takes only one parameter,  the $config hashref

=cut

sub create_noisefile {

	my $config = shift;

	print "\t\tChecking status of noise file\n";
	if ( ! -f "$config->{SEC_DIR}/noise"	 ) {
		print "\tGenerating noise file $config->{SEC_DIR}/noise\n";
		# generate noise file by piping the results of find /tmp to sha1sum
		my $ret = system("(find /tmp)|sha1sum > $config->{SEC_DIR}/noise");
		if ($ret > 0) {
			print "Error occured during noise file generation,  quitting\n\n";
			exit 1;
		}
	} else {
		print "\tUsing existing noise file\t--->$config->{SEC_DIR}/noise<---\n";
	}
}

##########
##  load_serial - load contents of the serial file to the $config hash.
##		this should be run in every sub that will be adding ANY cert to the db
##		so that the serials file stays sane.
##########

=head2 load_serial

Function to load up the $config->{key_*} lists with the serial file contents

Takes only one parameter,  the $config hashref

=cut


sub load_serial {

		my $config = shift;
	
		# TODO add a write check to the serial file
		# check if the serial file exists,  if not create one
		if ( ! -r $config->{SERIAL} ) {
			print "No serial file found at $config->{SERIAL},  creating one now\n";
			open SERIAL, ">$config->{SERIAL}";
			print SERIAL "0:HOSTNAME:CERT TYPE\n";
			close SERIAL;
		}
		# now check to make sure that file is readable/writeable.
		check_file($config->{SERIAL});

		# reset data in the lists 
		$config->{key_serial} = [];
		$config->{key_hostname} = [];
		$config->{key_description} = [];

		# suck in the serial file data
		open SERIAL, $config->{SERIAL};
		while (<SERIAL>) {
			my @tmp = split(/:/);
			
			push @{$config->{key_serial}}, $tmp[0];
			push @{$config->{key_hostname}}, $tmp[1];
			push @{$config->{key_description}}, $tmp[2];
		}
		close SERIAL;
}



##########
##  update_serial - update the serial file with contents of $config hash
##########

=head2 update_serial

Function to update the serials file with the contents of the $config->{key_*} lists 

Takes only one parameter,  the $config hashref

=cut


sub update_serial {
	# update the serials file.
	my $config = shift;
	# check file status
	check_file($config->{SERIAL});
	open SERIAL, ">$config->{SERIAL}";
	my $total = scalar(@{$config->{key_serial}});
	my $loop = 0;
	while ($loop < $total) {
		print SERIAL "$config->{key_serial}[$loop]:$config->{key_hostname}[$loop]:$config->{key_description}[$loop]";
		$loop+=1;
	}

	close SERIAL;
	
}


##########
##  checkCA - checks for existance of a CA Cert in the db.  return 1 if found,  0 if none found.
##########

sub checkCA {
	#certutil -L -d $secdir -n "CA certificate"
	my $config = shift;
	
	my $ret = system("$config->{CERTUTIL} -L -P slapd-$config->{INSTANCE}- -d $config->{SEC_DIR} -n \"CA certificate\" >/dev/null 2>&1");
	if ( $ret > 0 ) {
		# no cacert exists
		return 0;
	} else {
		# cacert exists
		return 1;
	}
}

##########
##  createCA - Creates a CA Cert in the database.
##########

sub createCA {
	
	my $config = shift;
	# check to make sure there isn't a CACert already
	if ( checkCA($config) ) {
		print "Already found a ca cert,  quitting\n";
		exit 1;
	} 
	# force a load of the serials file
	load_serial($config);
	create_pwdfile($config);
	create_noisefile($config);

	print "\t\tTrying to create CA cert\n";
	# next create CA cert
	my @createca = ("$config->{CERTUTIL}", "-S ", 
			"-P  slapd-$config->{INSTANCE}-",
			"-n \"CA certificate\"", 
			"-s \"cn=CAcert\"",
			"-x", "-t \"CT,,\"", 
			"-m " . scalar(@{$config->{key_serial}}),
			"-v $config->{EXPIRE}", 
			"-d $config->{SEC_DIR}", 
			"-z $config->{SEC_DIR}/noise", 
			"-f $config->{SEC_DIR}/pwdfile",
			"1>","/dev/null");

	my $ret = system("@createca");

	if ( $ret > 0 ) {
		print "\n\nError creating CAcert,  quitting\n";
		exit;
	} else {
		print "\t--->Succesfully created CAcert<---\n";
		# add new serial data to arrays and update serial file
		push @{$config->{key_serial}}, scalar(@{$config->{key_serial}});
		push @{$config->{key_hostname}}, hostname_long();
		push @{$config->{key_description}},"CACert\n" ;
		update_serial($config);
	}

	print "\t\tExporting CAcert key is asc form\n";
	# export the cacert to asc
	my @exportca = ("$config->{CERTUTIL}", "-L", 
			"-P slapd-$config->{INSTANCE}-",
			"-d $config->{SEC_DIR}",
			"-n \"CA certificate\"",
			"-a",
			">", "$config->{SEC_DIR}/cacert.asc");
	$ret = system("@exportca");
	if ( $ret > 0 ) {
		print "\n\nError generating $config->{SEC_DIR}/cacert.asc,  quitting\n\n";
		exit 1;
	} else {
		print "Succesfully created $config->{SEC_DIR}/cacert.asc\n";
	}


#    pk12util -d $secdir $prefixarg -o $secdir/cacert.p12 -n "CA certificate" -w $secdir/pwdfile.txt -k $secdir/pwdfile.txt
	print "\t\tExporting the CA key/cert to cacert.p12\t--->";
	my @keycert = ("$config->{PK12UTIL}",
			"-d $config->{SEC_DIR}",
			"-P slapd-$config->{INSTANCE}-",
			"-o $config->{SEC_DIR}/cacert.p12",
			"-n \"CA certificate\"",
			"-w $config->{SEC_DIR}/pwdfile",
			"-k $config->{SEC_DIR}/pwdfile");
	$ret = system("@keycert");

	if ( $ret > 0 ) {
		print "\n\nError generating $config->{SEC_DIR}/cacert.p12,  quitting\n\n";
		exit 1;
	} 

}

##########
##  createDB - create the security databases and create encryption key.
##########

sub createDB {
	
	my $config = shift;
	# initialize the pwdfile and noise file.
	create_pwdfile($config);
	create_noisefile($config);
	
	print "WARNING!!!! ABOUT TO DELETE OLD DATABASE FILES!!!!\n";
	print "the files are\n\n";
	print "\t$config->{SEC_DIR}/slapd-$config->{INSTANCE}-cert8.db\n\t$config->{SEC_DIR}/slapd-$config->{INSTANCE}-key3.db\n\t$config->{SEC_DIR}/secmod.db\n\n";
	print "Backup or move those files now if you want to keep them.\n\n";
	print "Enter to continue, CTRL-C to quit: ";
	chomp(my $delete =(<STDIN>));
	
      
	# delete old db files.
	unlink "$config->{SEC_DIR}/slapd-$config->{INSTANCE}-cert8.db";
	unlink "$config->{SEC_DIR}/slapd-$config->{INSTANCE}-key3.db";
	unlink "$config->{SEC_DIR}/secmod.db";
	print "\tGenerating new Database files";
	
	my @createdb = ("$config->{CERTUTIL}", "-N",
				"-d $config->{SEC_DIR}",
				"-f $config->{SEC_DIR}/pwdfile",
				"-P slapd-$config->{INSTANCE}-");

	my $ret = system("@createdb");

	      if ( $ret > 0 ) {
		    print "\n\nThere was an error creating database files, quitting\n\n";
		    exit 1;
	      } else {
		    print "\t--->Succesfully generated Database files\n";
	      }
      
	# now create database encryption key
	print "\t\tTrying to create encryption key\n";

	# create encryption key first
	my @createkey = ("$config->{CERTUTIL}","-G",
			"-P slapd-$config->{INSTANCE}-",
			"-d $config->{SEC_DIR}",
			"-z $config->{SEC_DIR}/noise",
			"-f $config->{SEC_DIR}/pwdfile",
			"1>","/dev/null");
	$ret = system("@createkey");

	if ( $ret > 0 ) {
		print "\n\nError generating encryption key,  quitting\n\n";
		exit 1;
	} else {
		print "\t--->Succesfully created encryption key<---\n";
	}
	

	# create symlinks too
	symlink("$config->{SEC_DIR}/slapd-$config->{INSTANCE}-key3.db","$config->{SEC_DIR}/key3.db");
	symlink("$config->{SEC_DIR}/slapd-$config->{INSTANCE}-cert8.db","$config->{SEC_DIR}/cert8.db");

	# find the user that fds runs at by looking at the ownership of the SEC_DIR
	my @stat = stat($config->{SEC_DIR});
	chown $stat[4], $stat[5], "$config->{SEC_DIR}/slapd-$config->{INSTANCE}-key3.db";
	chown $stat[4], $stat[5], "$config->{SEC_DIR}/slapd-$config->{INSTANCE}-cert8.db";
	chown $stat[4], $stat[5], "$config->{SEC_DIR}/secmod.db";
	chmod 0600, "$config->{SEC_DIR}/slapd-$config->{INSTANCE}-key3.db";
	chmod 0600, "$config->{SEC_DIR}/slapd-$config->{INSTANCE}-cert8.db";
	chmod 0600, "$config->{SEC_DIR}/secmod.db";
	
}

##########
##  listCerts - List all the certs in the database.  If passed a string,  assume we want details on that cert
##########

sub listCerts {
	
	my $config = shift;
	my $cert = shift;

	# list the certs in the database
	# will list all the cert names unless it recieves an argument with a Cert Name.
	my @listcert = ("$config->{CERTUTIL}", "-L",
			"-P slapd-$config->{INSTANCE}-",
			"-d $config->{SEC_DIR}");

	if (defined($cert)) {
		# add the -n argument to the listcert command
		push @listcert, "-n \"$cert\"";
	}

	my $ret = system("@listcert");
	if ( $ret > 0 ) {
		print "\n\nError listing certs,  perhaps you passed an invalid -d string? Try just -l\nQuitting\n\n";
		exit 1;
	}

}

##########
##  createCert - Create Server-Cert in the database.  MUST get passed at least a FULLY QUALIFIED DOMAIN NAME!
##		 with the option of including a comma separated list of altNames for the cert.
##########

sub createCert {

	my $config = shift;
	my ($fqdn, $altnamedns) = @_;
	my $cert_name;

	# force a load of the serials file,  and check noise/pwdfile
	load_serial($config);
	create_pwdfile($config);
	create_noisefile($config);
	# convert fqdn into hostname
	my @hostname = split(/\./,$fqdn);
	# check Server Cert name format
 	if ( $config->{SERV_CERT} ) {
		#define custom Server-Cert name here
			$cert_name = "Server-Cert-$hostname[0]";
		} else {
			$cert_name = "Server-Cert";
	}
	print "about to create a cert with a serial of " . scalar(@{$config->{key_serial}}) . "\n\n";
	my @createcert = ("$config->{CERTUTIL}", "-S",
			"-P slapd-$config->{INSTANCE}-",
			"-n $cert_name",
			"-s \"cn=$fqdn,ou=Fedora Directory Server\"",
			"-c \"CA certificate\"",
			"-t \"u,u,u\"",
			"-m ". scalar(@{$config->{key_serial}}),
			"-v $config->{EXPIRE}",
			"-d $config->{SEC_DIR}",
			"-z $config->{SEC_DIR}/noise",
			"-f $config->{SEC_DIR}/pwdfile");

	if (defined($altnamedns)) {
		# push the alt-dns entries onto the command array if arg was given
		push @createcert, "-8 $altnamedns";
	}

	my $ret = system("@createcert");
	if ( $ret > 0 ) {
		print "\n\nThere was an error creating $cert_name for $fqdn\n\n";
			exit 1;
		} else {
			print "\t--->Succesfully generated $cert_name for $fqdn\n";
			push @{$config->{key_serial}}, scalar(@{$config->{key_serial}});
			push @{$config->{key_hostname}}, $fqdn;
			push @{$config->{key_description}},"$cert_name\n" ;
			update_serial($config);
			
		}

}

##########
##  exportCert -  export a cert from the database
##		Must recieve a string with the cert to 
##		 
##########

sub exportCert {
	
	# TODO
	# the passed variable will need to have any whitespace escaped prior to it recieving it most likely
	my $config = shift;
	my $name = shift;
	
	my @extractcert = ("$config->{PK12UTIL}", "-o $config->{SEC_DIR}/$name.p12",
				"-d $config->{SEC_DIR}",
				"-P slapd-$config->{INSTANCE}-",
				"-n $name",
				"-k $config->{SEC_DIR}/pwdfile",
				"-w $config->{SEC_DIR}/pwdfile");

	my $ret = system("@extractcert");
	if ( $ret > 0 ) {
		print "\n\nError exporting cert for $name\n";
		print "Command executed - @extractcert\n";
		print "Quitting\n\n";
		exit 1;
	} else {
		print "\t\tSuccesfully wrote cert file to $config->{SEC_DIR}/$name.p12\n";
	}


}

##########
##  import_cert -  import a cert into the SECDIR database.
##		Must recieve a string with the cert to import
##		can be either p12 server-cert or cacert in .asc form
##		will also accept a file that is the pwd file needed to decode the cert.
##########
sub import_cert {

	my $config = shift;
	my ($cert, $pwdfile) = @_;
	
	# check that file exists and is readable/writeable.
	check_file($cert);

	my @importcert;
	# check to see if we're importing a CA or a Server cert
	if ( $cert =~ m/asc/ ) {
		#import the cacert in asc form
		print "passed an asc file\n";
		@importcert = ("$config->{CERTUTIL}", "-A",
				"-d $config->{SEC_DIR}",
				"-P slapd-$config->{INSTANCE}-",
				"-n \"CA certificate\"",
				"-t \"CT,,\"",
				"-a",
				"-i $cert");
	} 

	if ( $cert =~ m/p12/ ) {
		# import the p12 key
		print "passed the p12 file\n";
		#pk12util -i /tmp/Server-Cert-twisted/Server-Cert-twisted.p12 -d /etc/dirsrv/slapd-fds/ -P slapd-fds- -w /etc/dirsrv/slapd-fds/pwdfile-ca -k /etc/dirsrv/slapd-fds/pwdfile
		@importcert = ("$config->{PK12UTIL}",
				"-i $cert",
				"-d $config->{SEC_DIR}",
				"-P slapd-$config->{INSTANCE}-",
				"-k $config->{SEC_DIR}/pwdfile");
		# if we passed the CA's pwdfile then use it
		if (defined($pwdfile)) {
			# verify file exists
			if ( -r $pwdfile ) {
				# push argument @importcert
				push @importcert, "-w $pwdfile";
			} else {
				print "Unable to locate $pwdfile\n";
				print "You will be prompted below for the password used to encrypt the key you're importing\n";
				print "This is the contents of the pwdfile on the CAcert machine.\n\n";
			}
		} else {
			print "You will be prompted below for the password used to encrypt the key you're importing\n";
			print "This is the contents of the pwd file on the CAcert machine.\n\n";
		}

	}

	print "Importing key now\n\n";
	my $ret = system("@importcert");
}

sub enable_ssl {
	
	my $config = shift;
	my $server = shift;
	my $cert_name = shift;
	my ($ldap, $msg);
	
	print "server is $server and cert is $cert_name\n";

	if ( ($ldap = conn_bind($config, $server)) eq 0 ) {
		print "\t*********** bind/connect failed to $server ***************\n";
		return 0;
	}

	# create 2 entry objects,  one for cn=config,  the other for cn=encryption,cn=config
	$msg = $ldap->search(  filter=>"(cn=encryption)",
				base=> "cn=encryption,cn=config" );
	my $cn_encryption = $msg->entry;
	$msg = $ldap->search(  filter=>"(objectclass=nsslapdConfig)",
				base=> "cn=config" );
	my $cn_config = $msg->entry;
	
	
	# enable the various settings in cn=encryption,cn=config
	if ($cn_encryption->get_value ( 'nsSSL3' )) {
		print "Found existing attribute nsSSL3,  turning it on\n";
		$msg = $ldap->modify( "cn=encryption,cn=config", replace => {  'nsSSL3' => 'on' });
	} else {
		#ldapadd but shouldnt need to 
	}
	if ($cn_encryption->get_value ( 'nsSSLClientAuth' )) {
		print "Found existing attribute nsSSLClientAuth, setting it to allowed\n";
		$msg = $ldap->modify( "cn=encryption,cn=config", replace => { 'nsSSLClientAuth' => 'allowed' }  );
	} else {
		# ldapadd but shouldnt need to
	}
	
	if ($cn_encryption->get_value ( 'nsSSL3Ciphers' )) {
		print "nsSSLCiphers already configured,  skipping\n";
		
	} else {
		print "Adding nsSSLCiphers\n";
		$msg = $ldap->modify( "cn=encryption,cn=config", add => { 'nsSSL3Ciphers' => '-rsa_null_md5,+rsa_rc4_128_md5,+rsa_rc4_40_md5,+rsa_rc2_40_md5,+rsa_des_sha,+rsa_fips_des_sha,+rsa_3des_sha,+rsa_fips_3des_sha,+fortezza,+fortezza_rc4_128_sha,+fortezza_null,+tls_rsa_export1024_with_rc4_56_sha,+tls_rsa_export1024_with_des_cbc_sha,-rc4,-rc4export,-rc2,-rc2export,-des,-desede3' } );
	}
	# create the new RSA item
	$msg = $ldap->add ( "cn=RSA,cn=encryption,cn=config", attrs => [ 	'objectClass' => [ 'top' , 'nsEncryptionModule' ],
										'cn' => 'RSA' ,
										'nsSSLPersonalitySSL' => "$cert_name",
										'nsSSLToken' => 'internal (software)',
										'nsSSLActivation' => 'on' ]);
	
	
	#$cn_config->dump;
	# check to see if nsslapd-security exists,  if it does modify it,  if not create it
	if (defined($cn_config->get_value ('nsslapd-security'))) {
		print "Turning on nsslapd-descurity\n";
		$msg = $ldap->modify( "cn=config", replace => { 'nsslapd-security' => 'on'});
	} else {
		#ldapadd but shouldnt need to.
	}
	# check to see if nsslapd-ssl-check-hostname exists,  if it does modify it,  if not create it
	if (defined($cn_config->get_value ('nsslapd-ssl-check-hostname'))) {
		print "Turning off nsslapd-ssl-check-hostname\n";
		$msg = $ldap->modify( "cn=config", replace => { 'nsslapd-ssl-check-hostname' => 'off' });
	}

	# create pin file while we're at it.
	open PIN, ">$config->{SEC_DIR}/pin.txt";
	open PWD, "$config->{SEC_DIR}/pwdfile";
	while (<PWD>) {
		chomp;
		print PIN "Internal (Software) Token:$_";
	}
	close PIN;
	close PWD;

	my @stat = stat($config->{SEC_DIR});
	chown $stat[4], $stat[5], "$config->{SEC_DIR}/pin.txt";
	chmod 0600, "$config->{SEC_DIR}/pin.txt";
}


#######################
#####
#####	get_ - will return a portion of the config options to the calling script.
#####			
#######################

sub get_file_locations
{
	# build a new hashref with only the items we want in it,  then return it;
	my $config = shift;
	my @exports = qw/FDST_CONFIG FDST_DEFAULT_CONFIG PAM_SECRET NSS_SECRET CA_DIR SERIAL PK12UTIL CERTUTIL INSTANCE SEC_DIR/; 

	my $newref = {};
	# build new hashref
	foreach my $export (@exports) {
		#print "adding $export to the hashref\n";
		$newref->{$export} = $config->{$export};
	}
	
	return $newref;
}

sub get_replication_opts
{
	# build a new hashref with only the items we want in it,  then return it;
	my $config = shift;
	my @exports = qw/SUPPLIERS CONSUMERS BIND_DN REP_DN REP_ID BASE_DN DN_DESC/; 

	my $newref = {};
	# build new hashref
	foreach my $export (@exports) {
		#print "adding $export to the hashref\n";
		$newref->{$export} = $config->{$export};
	}
	
	return $newref;
}

=head1 AUTHOR

Ryan Braun, C<< <ryan.braun at ec.gc.ca> >>

=head1 BUGS

Please report any bugs or feature requests to C<bug-fdstools at rt.cpan.org>, or through
the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=fdstools>.  I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.




=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc fdstools


You can also look for information at:

=over 4

=item * RT: CPAN's request tracker

L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=fdstools>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/fdstools>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/fdstools>

=item * Search CPAN

L<http://search.cpan.org/dist/fdstools/>

=back


=head1 ACKNOWLEDGEMENTS


=head1 COPYRIGHT & LICENSE

Copyright 2008 Ryan Braun, all rights reserved.

This program is released under the following license: gpl


=cut

1; # End of fdstools
